/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ibm.moa.outmail;

import java.util.Timer;
import java.util.TimerTask;

import android.os.Handler;
import android.util.Log;

/**
 * This class used to "throttle" a flow of events.
 * 
 * When {@link #onEvent()} is called, it calls the callback in a certain timeout
 * later. Initially {@link #mMinTimeout} is used as the timeout, but if it gets
 * multiple {@link #onEvent} calls in a certain amount of time, it extends the
 * timeout, until it reaches {@link #mMaxTimeout}.
 * 
 * This class is primarily used to throttle content changed events.
 */
public class Throttle {
	public static final boolean DEBUG = false; // Don't submit with true

	public static final int DEFAULT_MIN_TIMEOUT = 150;
	public static final int DEFAULT_MAX_TIMEOUT = 2500;
	/* package */static final int TIMEOUT_EXTEND_INTERVAL = 500;

	private static Timer TIMER = new Timer();

	private final Clock mClock;
	private final Timer mTimer;

	/** Name of the instance. Only for logging. */
	private final String mName;

	/** Handler for UI thread. */
	private final Handler mHandler;

	/** Callback to be called */
	private final Runnable mCallback;

	/** Minimum (default) timeout, in milliseconds. */
	private final int mMinTimeout;

	/** Max timeout, in milliseconds. */
	private final int mMaxTimeout;

	/** Current timeout, in milliseconds. */
	private int mTimeout;

	/** When {@link #onEvent()} was last called. */
	private long mLastEventTime;

	private MyTimerTask mRunningTimerTask;

	/** Constructor with default timeout */
	public Throttle(String name, Runnable callback, Handler handler) {
		this(name, callback, handler, DEFAULT_MIN_TIMEOUT, DEFAULT_MAX_TIMEOUT);
	}

	/** Constructor that takes custom timeout */
	public Throttle(String name, Runnable callback, Handler handler,
			int minTimeout, int maxTimeout) {
		this(name, callback, handler, minTimeout, maxTimeout, Clock.INSTANCE,
				TIMER);
	}

	/** Constructor for tests */
	/* package */Throttle(String name, Runnable callback, Handler handler,
			int minTimeout, int maxTimeout, Clock clock, Timer timer) {
		if (maxTimeout < minTimeout) {
			throw new IllegalArgumentException();
		}
		mName = name;
		mCallback = callback;
		mClock = clock;
		mTimer = timer;
		mHandler = handler;
		mMinTimeout = minTimeout;
		mMaxTimeout = maxTimeout;
		mTimeout = mMinTimeout;
	}

	private void debugLog(String message) {
		Log.d("mail", "Throttle: [" + mName + "] " + message);
	}

	private boolean isCallbackScheduled() {
		return mRunningTimerTask != null;
	}

	public void cancelScheduledCallback() {
		if (mRunningTimerTask != null) {
			if (DEBUG)
				debugLog("Canceling scheduled callback");
			mRunningTimerTask.cancel();
			mRunningTimerTask = null;
		}
	}

	/* package */void updateTimeout() {
		final long now = mClock.getTime();
		if ((now - mLastEventTime) <= TIMEOUT_EXTEND_INTERVAL) {
			mTimeout *= 2;
			if (mTimeout >= mMaxTimeout) {
				mTimeout = mMaxTimeout;
			}
			if (DEBUG)
				debugLog("Timeout extended " + mTimeout);
		} else {
			mTimeout = mMinTimeout;
			if (DEBUG)
				debugLog("Timeout reset to " + mTimeout);
		}

		mLastEventTime = now;
	}

	public void onEvent() {
		if (DEBUG)
			debugLog("onEvent");

		updateTimeout();

		if (isCallbackScheduled()) {
			if (DEBUG)
				debugLog("    callback already scheduled");
		} else {
			if (DEBUG)
				debugLog("    scheduling callback");
			mRunningTimerTask = new MyTimerTask();
			mTimer.schedule(mRunningTimerTask, mTimeout);
		}
	}

	/**
	 * Timer task called on timeout,
	 */
	private class MyTimerTask extends TimerTask {
		private boolean mCanceled;

		@Override
		public void run() {
			mHandler.post(new HandlerRunnable());
		}

		@Override
		public boolean cancel() {
			mCanceled = true;
			return super.cancel();
		}

		private class HandlerRunnable implements Runnable {
			@Override
			public void run() {
				mRunningTimerTask = null;
				if (!mCanceled) { // This check has to be done on the UI thread.
					if (DEBUG)
						debugLog("Kicking callback");
					mCallback.run();
				}
			}
		}
	}

	/* package */int getTimeoutForTest() {
		return mTimeout;
	}

	/* package */long getLastEventTimeForTest() {
		return mLastEventTime;
	}
}
